Skip to content

[Feature][C++] Support mmap as an optional local file read backend - #920

Open
Young-Leo wants to merge 1 commit into
apache:developfrom
Young-Leo:ly/mmap-read-backend
Open

[Feature][C++] Support mmap as an optional local file read backend#920
Young-Leo wants to merge 1 commit into
apache:developfrom
Young-Leo:ly/mmap-read-backend

Conversation

@Young-Leo

@Young-Leo Young-Leo commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add AUTO, MMAP, and PREAD local file read backends for the C++ reader.
  • Implement POSIX mmap and Windows file mapping.
  • Support automatic fallback and explicit mapping errors.
  • Expose the read backend configuration through the C and Python APIs.
  • Add correctness and lifecycle tests, documentation, and a read-backend benchmark.

Backend behavior

  • AUTO prefers memory mapping for supported regular files and falls back to the positioned-read backend when mapping is unavailable.
  • MMAP requires memory mapping and returns an explicit error if the file cannot be mapped.
  • PREAD preserves the existing positioned-read behavior.
  • The configured backend is captured when a reader opens a file, so changing the global configuration does not affect existing readers.
  • A file must not be modified or truncated while it is open through the MMAP backend.

Benchmark

These are preliminary Windows Release results. They are not used as a performance threshold because relative performance depends on the access pattern, cache state, filesystem, storage device, and hardware.

Environment and protocol

  • Platform: Windows 11
  • Compiler: MinGW-w64 GCC 15.2
  • Build: Release, -O3
  • CPU: 16 logical processors
  • Memory: 31.73 GiB
  • Dataset: 8 TsFiles totaling 67.27 MiB
  • Low-level access: single-threaded 64 KiB sequential ReadFile::read() scans
  • Cache-state benchmark: 12 balanced rounds per state, alternating PREAD/MMAP execution order
  • DataFrame benchmark: 6 rounds, 4 sequential epochs, and 32 random batches per round

Cache-state study

The low-level benchmark separates three different cache and mapping states:

Cache state PREAD (MiB/s) MMAP (MiB/s) MMAP / PREAD PREAD page faults MMAP page faults
Approximate cold file cache 1,056.3 1,234.1 1.17x 0 17,233
Warm file cache, new mapping 6,025.1 2,601.9 0.43x 0 17,233
Warm file cache, pre-touched mapping 5,831.9 17,440.3 2.99x 0 0

Note: PageFaultCount is a process metric, not a disk I/O metric. PREAD copies file data through the kernel file cache into a user buffer, so file-page faults are generally not counted for the calling process. MMAP accesses mapped pages directly, so first-touch faults are counted for the process. Therefore, a PREAD value of zero does not mean that no disk I/O occurred.

The last two rows differ only in whether the same MMAP view was completely traversed before timing.

With a newly created mapping, the first sequential traversal incurred approximately 17,233 page faults and reached only 0.43x the throughput of PREAD. After pre-touching the same mapping, these page faults disappeared and MMAP throughput increased from approximately 2.54 GiB/s to 17.03 GiB/s, reaching 2.99x the PREAD throughput.

This explains why MMAP may be slower for the first complete sequential scan even when the OS file cache is already warm: the process still needs to establish page-table mappings when it touches the MMAP pages for the first time.

The approximate cold-cache state used a unique CopyFileExW(..., COPY_FILE_NO_BUFFERING) copy for each round instead of clearing the machine-wide standby list. Copy preparation time was excluded from the measured throughput. Storage-controller caches, filesystem metadata caches, and background system activity may still affect this result.

Windows PageFaultCount is a process-wide total and does not directly distinguish soft and hard page faults. In the warm-file-cache experiment, the MMAP page faults are inferred to be primarily soft faults because every file was completely scanned before opening the measured backend.

The current MMAP implementation still copies mapped bytes into the caller-provided buffer in ReadFile::read(), so this is not a zero-copy benchmark.

TsFileDataFrame end-to-end benchmark

The end-to-end benchmark represents a training-style workload:

  • 8 TsFiles
  • 1 device per file
  • 8 DOUBLE measurements per file
  • 131,072 timestamp rows per file
  • 64 output columns in total
  • 8,192 rows × 64 columns per batch
  • 16 batches per sequential epoch
  • 6 rounds per backend configuration
  • 4 sequential epochs per round
  • 32 random batches per round
Query workers Phase PREAD (rows/s) MMAP (rows/s) MMAP / PREAD
1 First sequential epoch, new reader 370,618 471,046 1.27x
1 Random windows, warm mapping 377,048 489,595 1.30x
1 Repeated sequential epochs 2–4 381,496 493,970 1.29x
4 First sequential epoch, new reader 631,136 706,415 1.12x
4 Random windows, warm mapping 646,104 722,667 1.12x
4 Repeated sequential epochs 2–4 653,082 727,808 1.11x

For this training-style workload, MMAP improved end-to-end throughput by:

  • approximately 27–30% with one query worker;
  • approximately 11–12% with four query workers.

The relative improvement becomes smaller with four workers because decompression, decoding, NumPy materialization, scheduling, and parallel-query overhead account for a larger part of the total runtime.

Unlike the low-level sequential scan, the DataFrame reader performs many fine-grained metadata, chunk, and page reads. MMAP avoids repeated positioned-read calls in this access pattern, so it still improves the first DataFrame epoch despite the initial page-touch overhead.

Native stack-sampling observations

Periodic GDB native stack sampling was collected for both backends:

  • Low-level PREAD scans contained ReadFile/NtReadFile in 94–99% of captured stacks.
  • The corresponding MMAP scans contained no ReadFile/NtReadFile stacks and were dominated by the memory-copy path.
  • In the DataFrame workload, ReadFile/NtReadFile appeared in 8.5–9.9% of PREAD stacks and in none of the MMAP stacks.
  • Value-page decoding appeared in approximately 23–32% of DataFrame stacks for both backends.

These results confirm that the MMAP backend removes the repeated positioned-read path, while decompression and decoding remain substantial end-to-end costs.

The profiles were collected through periodic GDB interruption rather than ETW hardware sampling. Stack percentages describe the presence of a function in captured stacks and should not be interpreted as direct elapsed-time percentages.

Interpretation

MMAP is not faster for every workload.

A newly created mapping can be slower for a one-time sequential scan because the first traversal must establish the process page-table mappings. MMAP is more beneficial when:

  • the same mapping is reused across multiple epochs;
  • the workload performs many small or random reads;
  • metadata, chunk, and page ranges are accessed repeatedly;
  • a training or analytical workload repeatedly queries the same TsFiles.

PREAD can remain preferable for one-time sequential scans. Users can explicitly select PREAD for that access pattern.

AUTO is a compatibility-oriented fallback policy rather than a workload-adaptive performance policy: it prefers MMAP for supported regular files and falls back to PREAD only when mapping is unavailable.

Validation

  • Added C++ tests for positioned reads, bounded MMAP reads, backend selection, resource release, repeated close, automatic fallback, required-MMAP errors, unsupported mapping, empty files, and invalid configuration.
  • Added C API configuration round-trip and validation tests.
  • Added Python API configuration and query-equivalence tests.
  • C++ Spotless formatting completed successfully.
  • The cache-state benchmark completed 72 formal low-level trials.
  • The TsFileDataFrame benchmark completed 24 formal end-to-end trials.
  • Eight PREAD/MMAP native stack profiles were collected and validated.

Closes #903

Add AUTO, MMAP, and PREAD configuration across the C++, C, and Python APIs. Implement Windows and POSIX mappings with fallback and explicit errors, plus lifecycle tests, documentation, and comparison benchmarks for issue apache#903.
@ColinLeeo
ColinLeeo requested a lite review from Copilot August 26, 2026 09:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Adds configurable local-file read backends (AUTO/MMAP/PREAD) with mmap/file-mapping support across C++, C, and Python APIs, plus tests and benchmarking/docs to validate behavior and performance characteristics.

Changes:

  • Introduces FileReadBackend selection (process-wide) and wires it into ReadFile open/read paths with AUTO fallback vs required-MMAP errors.
  • Exposes backend configuration via C wrapper + Python bindings (config dict + dedicated get/set functions) and adds a new mapping error code.
  • Adds C++/Python tests, updates CLIs/docs, and adds an optional CMake benchmark target.

Reviewed changes

Copilot reviewed 29 out of 29 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
python/tsfile/tsfile_py_cpp.pyx Exposes backend get/set APIs and includes backend in config dict.
python/tsfile/tsfile_py_cpp.pxd Exports new Python C-API symbols for backend get/set.
python/tsfile/tsfile_cpp.pxd Declares C wrapper enum + functions for backend configuration.
python/tsfile/exceptions.py Adds FILE_MAP_ERROR and maps it to FileMapError.
python/tsfile/constants.py Introduces FileReadBackend IntEnum.
python/tsfile/init.py Re-exports backend configuration helpers.
python/tests/test_write_and_read.py Adds Python configuration round-trip + equivalence test for PREAD/MMAP.
python/tests/test_exceptions.py Adds test coverage for the new FileMapError mapping.
python/README.md Documents Python backend configuration and behavior caveats.
python/README-zh.md Chinese docs for Python backend configuration and caveats.
cpp/tools/format/output_format.cc Adds CLI-readable message for E_FILE_MAP_ERR.
cpp/test/tools/output_format_test.cc Tests new error-message mapping for E_FILE_MAP_ERR.
cpp/test/file/read_file_test.cc Adds lifecycle/behavior tests for AUTO/MMAP/PREAD and fallback/errors.
cpp/test/cwrapper/cwrapper_test.cc Adds C API backend config round-trip/validation test.
cpp/src/utils/injection.h Exposes test-only injection enable/disable API.
cpp/src/utils/errno_define.h Adds E_FILE_MAP_ERR error code.
cpp/src/file/read_file.h Extends ReadFile with backend selection + mapping state.
cpp/src/file/read_file.cc Implements mapping/unmapping + backend-aware read/generation logic.
cpp/src/cwrapper/tsfile_cwrapper.h Documents/exposes C API for backend selection.
cpp/src/cwrapper/tsfile_cwrapper.cc Implements C API backend config functions.
cpp/src/cwrapper/errno_define_c.h Exposes RET_FILE_MAP_ERR for C API clients.
cpp/src/common/global.h Declares global backend getter/setter (outside ConfigValue for ABI).
cpp/src/common/global.cc Implements atomic global backend config + test injection helpers.
cpp/src/common/config/config.h Defines common::FileReadBackend enum.
cpp/bench_mark/bench_mark_src/read_backend_benchmark.cc Adds benchmark tool to compare MMAP vs PREAD workloads.
cpp/bench_mark/README.md Documents benchmark build/run protocol and interpretation.
cpp/README.md Documents backend selection behavior + return codes in C/C++.
cpp/README-zh.md Chinese docs for backend selection behavior + return codes.
cpp/CMakeLists.txt Adds BUILD_BENCHMARK option and benchmark target wiring.
Suppressed comments (1)

python/tsfile/exceptions.py:1

  • get_exception(...) (per the provided context) formats the exception message via ERROR_MESSAGES.get(code, "Unknown library error") and passes it as context. This diff adds FileMapError to ERROR_MAPPING, but does not add a corresponding entry to ERROR_MESSAGES, which likely means clients will see "Unknown library error" for code 55 instead of the intended “Failed to memory-map file”. Add the missing ERROR_MESSAGES[55] entry (and consider asserting the message in python/tests/test_exceptions.py so this doesn’t regress).
# Licensed to the Apache Software Foundation (ASF) under one

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 883 to 887
cpdef void set_tsfile_config(dict new_config):
if "file_read_backend_" in new_config:
set_file_read_backend(new_config["file_read_backend_"])
if "tsblock_mem_inc_step_size_" in new_config:
_check_uint32(new_config["tsblock_mem_inc_step_size_"])
Comment thread cpp/src/file/read_file.cc
ret = map_ret;
} else {
LOGW("mmap unavailable for " << file_path_.c_str()
<< "; falling back to pread");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature][C++] Support mmap as an optional local file read backend

2 participants